package org.xx.armory.commons;

import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter.Indenter;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import static com.fasterxml.jackson.core.JsonGenerator.Feature.AUTO_CLOSE_TARGET;
import static com.fasterxml.jackson.core.JsonGenerator.Feature.FLUSH_PASSED_TO_STREAM;
import static java.lang.Character.isUpperCase;
import static java.lang.reflect.Array.get;
import static java.lang.reflect.Array.getLength;
import static java.lang.reflect.Modifier.isPublic;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.fill;
import static org.xx.armory.commons.Validators.notNull;

/**
 * Json相关的工具方法。
 *
 * @author Haart
 */
public final class JsonUtils {
    private static final Map<Class<?>, Map<String, Method>> PROPS_CACHE = new ConcurrentHashMap<>();

    /**
     * 此集合中的类型名，看作基元类型，直接调用 {@link Object#toString()} 输出。
     */
    private static final Set<String> CLASSES_AS_PRIMITIVE_TYPE = createPrimitiveClasses();

    private static final String JSON_NULL = "null";

    /**
     * 禁止构造JsonUtils的实例。
     */
    private JsonUtils() {
    }

    private static Set<String> createPrimitiveClasses() {
        final Set<String> ret = new HashSet<>();

        ret.add("org.apache.logging.log4j.Level");

        return ret;
    }

    private static JsonFactory createJsonFactory() {
        return new JsonFactory().enable(FLUSH_PASSED_TO_STREAM);
    }

    private static DefaultPrettyPrinter createDefaultPretty() {
        NewLineIndent.initOnce();

        return new DefaultPrettyPrinter().withObjectIndenter(new NewLineIndent());
    }


    /**
     * 将对象转化为JSON字符串。
     *
     * @param obj
     *         待输出的对象。
     * @return 转化后的JSON字符串，如果参数{@code obj}是{@code null}则返回字符串{@literal "null"}。
     * @throws UncheckedIOException
     *         如果创建JSON字符串失败。
     */
    public static String toJson(
            Object obj
    ) {
        if (obj == null) {
            return JSON_NULL;
        } else if (obj instanceof Optional) {
            final Optional<?> opt = (Optional<?>) obj;
            return opt.isPresent() ? toJson(opt.get()) : JSON_NULL;
        } else {
            final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            try (final JsonGenerator jg = Constants.FACTORY
                    .createGenerator(buffer, JsonEncoding.UTF8)
                    .enable(AUTO_CLOSE_TARGET)
                    .setPrettyPrinter(Constants.PRETTY.createInstance())) {
                writeJsonObject(jg, obj);
            } catch (IOException ex) {
                throw new UncheckedIOException(ex);
            }

            try {
                return buffer.toString(UTF_8.name());
            } catch (UnsupportedEncodingException ex) {
                throw new UncheckedIOException(ex);
            }
        }
    }

    /**
     * 以JSON格式输出对象。
     *
     * @param jsonGenerator
     *         JSON生成器对象。
     * @param obj
     *         待输出的对象。
     * @throws UncheckedIOException
     *         如果输出对象时出现错误。
     * @throws IllegalArgumentException
     *         如果参数{@code jsonGenerator}是{@code null}。
     */
    public static void writeJsonObject(
            JsonGenerator jsonGenerator,
            Object obj
    ) {
        notNull(jsonGenerator, "jsonGenerator");

        try {
            if (obj == null) {
                jsonGenerator.writeNull();
            } else if (obj instanceof BigDecimal) {
                jsonGenerator.writeString(((BigDecimal) obj).toPlainString());
            } else if (obj instanceof Number) {
                final Number n = (Number) obj;
                if (n instanceof Integer) {
                    jsonGenerator.writeNumber(n.intValue());
                } else if (n instanceof Long) {
                    jsonGenerator.writeNumber(n.longValue());
                } else if (n instanceof Double) {
                    jsonGenerator.writeNumber(n.doubleValue());
                } else if (n instanceof Float) {
                    jsonGenerator.writeNumber(n.floatValue());
                } else if (n instanceof Short) {
                    jsonGenerator.writeNumber(n.shortValue());
                } else if (n instanceof Byte) {
                    jsonGenerator.writeNumber(n.byteValue());
                } else if (n instanceof BigInteger) {
                    jsonGenerator.writeNumber((BigInteger) n);
                } else if (n instanceof AtomicInteger) {
                    jsonGenerator.writeNumber(((AtomicInteger) n).get());
                } else if (n instanceof AtomicLong) {
                    jsonGenerator.writeNumber(((AtomicLong) n).get());
                }
            } else if (obj instanceof Character || obj instanceof CharSequence) {
                jsonGenerator.writeString(obj.toString());
            } else if (obj instanceof Boolean) {
                jsonGenerator.writeBoolean((Boolean) obj);
            } else if (obj instanceof AtomicBoolean) {
                jsonGenerator.writeBoolean(((AtomicBoolean) obj).get());
            } else if (obj instanceof Date) {
                jsonGenerator.writeNumber(((Date) obj).getTime());
            } else if (obj.getClass().isEnum() && obj instanceof ValueEnum) {
                jsonGenerator.writeNumber(((ValueEnum) obj).value());
            } else if (obj instanceof byte[]) {
                jsonGenerator.writeBinary((byte[]) obj);
            } else if (obj.getClass().isArray()) {
                writeJsonArray(jsonGenerator, obj);
            } else if (obj instanceof Collection<?>) {
                writeJsonArray(jsonGenerator, ((Collection<?>) obj).toArray());
            } else if (obj instanceof Map<?, ?>) {
                writeJsonMap(jsonGenerator, (Map<?, ?>) obj);
            } else if (CLASSES_AS_PRIMITIVE_TYPE.contains(obj.getClass().getCanonicalName())) {
                jsonGenerator.writeString(obj.toString());
            } else {
                writeJsonMap(jsonGenerator, toPropsMap(obj));
            }
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }

    /**
     * 以JSON格式输出数组。
     *
     * @param jsonGenerator
     *         JSON生成器对象。
     * @param array
     *         待输出的数组。
     * @throws UncheckedIOException
     *         如果输出数组时出现错误。
     * @throws IllegalArgumentException
     *         如果参数{@code jg}或者{@code array}是{@code null}。
     */
    private static void writeJsonArray(
            JsonGenerator jsonGenerator,
            Object array
    ) {
        notNull(jsonGenerator, "jsonGenerator");
        notNull(array, "array");

        try {
            jsonGenerator.writeStartArray();
            final int size = getLength(array);
            for (int i = 0; i < size; ++i) {
                writeJsonObject(jsonGenerator, get(array, i));
            }
            jsonGenerator.writeEndArray();
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }

    /**
     * 以JSON格式输出映射。
     *
     * @param jsonGenerator
     *         JSON生成器对象。
     * @param map
     *         待输出的映射。
     * @throws UncheckedIOException
     *         如果输出映射时出现错误。
     * @throws IllegalArgumentException
     *         如果参数{@code jg}或者{@code map}是{@code null}。
     */
    private static void writeJsonMap(
            JsonGenerator jsonGenerator,
            Map<?, ?> map
    ) {
        notNull(jsonGenerator, "jsonGenerator");
        notNull(map, "map");

        try {
            jsonGenerator.writeStartObject();
            for (final Map.Entry<?, ?> entry : map.entrySet()) {
                jsonGenerator.writeFieldName(String.valueOf(entry.getKey()));
                writeJsonObject(jsonGenerator, entry.getValue());
            }
            jsonGenerator.writeEndObject();
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }

    /**
     * 将对象转化为映射。
     *
     * @param obj
     *         待转化的对象。
     * @return 映射，包含了所有可以被获取的属性。
     * @throws IllegalArgumentException
     *         如果参数{@code obj}是{@code null}。
     * @throws IllegalStateException
     *         如果调用对象的方法获取属性值时出现反射错误。
     */
    private static Map<String, Object> toPropsMap(
            Object obj
    ) {
        notNull(obj, "obj");

        final Map<String, Object> ret = new LinkedHashMap<>();

        final Map<String, Method> accessors = PROPS_CACHE
                .computeIfAbsent(obj.getClass(), JsonUtils::getPropAccessors);

        for (final Map.Entry<String, Method> accessor : accessors.entrySet()) {
            final String key = accessor.getKey();
            final Method method = accessor.getValue();

            method.setAccessible(true);

            final Object value;
            try {
                value = method.invoke(obj, (Object[]) null);
            } catch (ReflectiveOperationException ex) {
                throw new IllegalStateException("cannot invoke method", ex);
            }

            if (value != null) {
                ret.put(key, value);
            }
        }

        return ret;
    }

    /**
     * 根据类型获取所有的属性读访问器。
     *
     * <p>
     * 只包括公共属性访问器。如果{@code clazz}是枚举类型或者是基本类型则不查询父类的属性访问器，否则也包含父类的属性访问器。
     * </p>
     *
     * @param clazz
     *         类型。
     * @return 所有的属性读访问器的映射，key是属性的名字，value是访问器方法。
     * @throws NullPointerException
     *         如果参数{@code clazz}是{@code null}。
     */
    public static Map<String, Method> getPropAccessors(
            Class<?> clazz
    ) {
        notNull(clazz, "clazz");

        final Map<String, Method> ret = new TreeMap<>();

        final boolean includeSuperClass = !clazz.isEnum() && clazz.getClassLoader() != null;
        final Method[] methods = includeSuperClass ? clazz.getMethods() : clazz
                .getDeclaredMethods();
        for (final Method method : methods) {
            if (isPublic(method.getModifiers())) {
                String name = method.getName();
                String key;
                if (name.startsWith("get")) {
                    if ("getClass".equals(name) || "getDeclaringClass".equals(name)) {
                        key = "";
                    } else {
                        key = name.substring(3);
                    }
                } else if (name.startsWith("is")) {
                    key = name.substring(2);
                } else {
                    key = "";
                }
                if (key.length() > 0 && isUpperCase(key.charAt(0)) && method.getParameterTypes().length == 0) {
                    if (key.length() == 1) {
                        key = key.toLowerCase();
                    } else {
                        key = key.substring(0, 1).toLowerCase() + key.substring(1);
                    }

                    ret.put(key, method);
                }
            }
        }

        return ret;
    }

    private static class Constants {
        static final JsonFactory FACTORY = createJsonFactory();
        static final DefaultPrettyPrinter PRETTY = createDefaultPretty();
    }

    private static class NewLineIndent
            implements Indenter {
        final static int SPACE_COUNT = 64;
        final static char[] SPACES = new char[SPACE_COUNT];

        static void initOnce() {
            fill(SPACES, ' ');
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void writeIndentation(
                JsonGenerator jsonGenerator,
                int level
        )
                throws IOException {
            jsonGenerator.writeRaw('\n');
            level += level;
            while (level > SPACE_COUNT) {
                jsonGenerator.writeRaw(SPACES, 0, SPACE_COUNT);
                level -= SPACES.length;
            }
            jsonGenerator.writeRaw(SPACES, 0, level);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public final boolean isInline() {
            return false;
        }
    }
}
